package pers.lipeng.greedy_snake;

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @author APengG
 * 该类直接继承JFrame，用于创建游戏窗口
 * 使用该类时便可以直接创建一个游戏窗口
 * 通过构造方法初始化窗口
 */
public class GreedySnakeGame extends JFrame {
    public static void main(String[] args) {
        //入口
        new GreedySnakeGame();
    }

    /**
     * 游戏面板，用于显示游戏内容
     */
    private JPanel gamePanel;
    /**
     * 贪吃蛇背板行数和列数
     */
    private final static int ROWS = 50;
    private final static int COLS = 50;
    /**
     * 存储每一个格子，方便后面修改每一个格子的属性
     */
    private JPanel[][] backPanels = new JPanel[ROWS][COLS];
    private List<GreedySnakeBody> snakeBodies = new ArrayList<>();
    /**
     * 蛇移动方向
     * 默认向下移动
     */
    private GreedySnakeMoveDirection currentDirection = GreedySnakeMoveDirection.DOWN;
    /**
     * 定时器
     */
    private Timer timer;
    /**
     * 是否可以改变方向
     */
    private AtomicBoolean isPause = new AtomicBoolean(true);
    private Food food = new Food(0, 0);
    /**
     * 分数
     */
    private int scoreNum = 0;
    /**
     * 分数标签
     */
    private JLabel score = new JLabel("分数：" + scoreNum);
    /**
     * 分数面板用于显示分数或者其他信息
     */
    private JPanel scorePanel;

    public GreedySnakeGame() {
        //设置窗口标题等同于this.setTitle("贪吃蛇");
        super("贪吃蛇");

        //初始化游戏面板
        gamePanel = new JPanel();
        //设置游戏面板布局
        GridLayout gridLayout = new GridLayout(ROWS, COLS, 0, 0);
        gamePanel.setLayout(gridLayout);
        //初始化游戏背板
        for (int i = 0; i < ROWS * COLS; i++) {
            //创建一个面板
            JPanel panel = new JPanel();
            panel.setLayout(new BorderLayout());
//            //设置面板背景颜色,此处设置为黑白相间为了方便观察此时的布局样式后期删除即可
//            if((i + (i / COLS)) % 2 == 0){
//                panel.setBackground(Color.WHITE);
//            }else{
//                panel.setBackground(Color.BLACK);
//            }
            //设置每个格子的大小为长15像素，宽15像素
            panel.setPreferredSize(new Dimension(15, 15));
            //将面板添加到游戏面板中
            gamePanel.add(panel);
            //将面板添加到背板数组中
            backPanels[i % ROWS][i / COLS] = panel;
        }

        //初始化贪吃蛇,蛇的初始长度为6。使用递减为了蛇头向下
        for (int i = 5; i >= 0; i--) {
            //蛇的坐标在背板中的位置，
            GreedySnakeBody greedySnakeBody =
                    new GreedySnakeBody(ROWS / 2, i);
            snakeBodies.add(greedySnakeBody);
        }
        paintSnake();
        this.addKeyListener(new SnakeMoveListener());
        loaderTimer();

        scorePanel = new JPanel();
        scorePanel.setLayout(new GridLayout(1,3,0,0));
        scorePanel.setBackground(Color.WHITE);
        scorePanel.add(score);
        this.setLayout(new BorderLayout());
        //将计分面板添加到窗口中
        this.getContentPane().add(scorePanel,BorderLayout.NORTH);
        //将游戏面板添加到窗口中
        this.getContentPane().add(gamePanel,BorderLayout.SOUTH);
        //先设置窗口大小下边才能获取窗口宽度
        this.setSize(900, 720);
        //获取当前屏幕大小
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        //等同于 (screenSize.width - this.getWidth()) / 2 目的为了让窗口横向居中
        int x = screenSize.width - this.getWidth() >> 1;
        this.setLocation(x, 0);
        //设置窗口大小不可变，即使用鼠标无法拖动改变窗口大小
        this.setResizable(false);
        //设置窗口关闭方式，即点击关闭按钮时只关闭当前窗口
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        //设置窗口可见,不设置窗口不显示
        this.setVisible(true);
        //设置紧凑布局，即窗口大小为游戏面板大小
        this.pack();

        randomProduceFood();
    }

    private void loaderTimer() {
        if (timer != null) {
            timer.stop();
        }
        //初始化定时器
        //500表示500ms执行一次方法
        timer = new Timer(150, e -> {
            if (currentDirection == GreedySnakeMoveDirection.UP) {
                snakeMoveUp();
            }
            if (currentDirection == GreedySnakeMoveDirection.DOWN) {
                snakeMoveDown();
            }
            if (currentDirection == GreedySnakeMoveDirection.LEFT) {
                snakeMoveLeft();
            }
            if (currentDirection == GreedySnakeMoveDirection.RIGHT) {
                snakeMoveRight();
            }
            isPause.set(true);
        });
        //启动定时器
        timer.start();
    }

    private void paintSnake() {
        for (GreedySnakeBody snakeBody : snakeBodies) {
            //获取蛇的坐标
            int bodyX = snakeBody.getBodyX();
            int bodyY = snakeBody.getBodyY();
            //获取蛇应该在的面板位置
            JPanel panel = backPanels[bodyX][bodyY];
            //加入面板并设置边框布局使蛇的身体填满面板
            panel.add(snakeBody, BorderLayout.CENTER);
        }
    }

    class SnakeMoveListener extends KeyAdapter {
        @Override
        public void keyPressed(KeyEvent e) {
            //判断是否可以改变方向
            if (isPause.get()) {
                //按下W或上键，向上移动
                if (e.getKeyCode() == KeyEvent.VK_W || e.getKeyCode() == KeyEvent.VK_UP) {
                    if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.VERTICAL.getVar()) {
                        currentDirection = GreedySnakeMoveDirection.UP;
                    }
                }
                //按下S或下键，向下移动
                if (e.getKeyCode() == KeyEvent.VK_S || e.getKeyCode() == KeyEvent.VK_DOWN) {
                    if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.VERTICAL.getVar()) {
                        currentDirection = GreedySnakeMoveDirection.DOWN;
                    }
                }
                //按下A或左键，向左移动
                if (e.getKeyCode() == KeyEvent.VK_A || e.getKeyCode() == KeyEvent.VK_LEFT) {
                    if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.HORIZONTAL.getVar()) {
                        currentDirection = GreedySnakeMoveDirection.LEFT;
                    }
                }
                //按下D或右键，向右移动
                if (e.getKeyCode() == KeyEvent.VK_D || e.getKeyCode() == KeyEvent.VK_RIGHT) {
                    if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.HORIZONTAL.getVar()) {
                        currentDirection = GreedySnakeMoveDirection.RIGHT;
                    }
                }
                //改变方向后设置为不可改变方向，必须定时器执行完毕后才能改变方向
                isPause.set(false);
            }
        }
    }

    private void snakeMoveUp() {
        //获取当前蛇头
        GreedySnakeBody snakeHead = snakeBodies.get(0);
        //获取蛇头的坐标
        int headX = snakeHead.getBodyX();
        int headY = snakeHead.getBodyY();
        //移动蛇头
        int nextY = headY - 1;
        //判断是否撞墙
        if (nextY < 0) {
            //撞墙了，游戏结束
            System.out.println("撞墙了，游戏结束");
            gameOverDeal(GreedySnakeGameOverResult.HIT_WALL);
            return;
        }
        //判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
        if (backPanels[headX][nextY].getComponentCount() > 0 &&
                backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody) {
            //撞到自己了，游戏结束
            System.out.println("撞到自己了，游戏结束");
            gameOverDeal(GreedySnakeGameOverResult.HIT_SELF);
            return;
        }
        //判断是否吃到食物
        ifEatFood(headX, nextY);
        //更新蛇的身体
        updateSnakeBodyList(headX, nextY);
        //重绘蛇
        paintSnake();
    }

    private void snakeMoveDown() {
        //获取当前蛇头
        GreedySnakeBody snakeHead = snakeBodies.get(0);
        //获取蛇头的坐标
        int headX = snakeHead.getBodyX();
        int headY = snakeHead.getBodyY();
        //移动蛇头
        int nextY = headY + 1;
        //判断是否撞墙
        if (nextY >= ROWS) {
            //撞墙了，游戏结束
            System.out.println("撞墙了，游戏结束");
            gameOverDeal(GreedySnakeGameOverResult.HIT_WALL);
            return;
        }
        //判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
        if (backPanels[headX][nextY].getComponentCount() > 0 &&
                backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody) {
            //撞到自己了，游戏结束
            System.out.println("撞到自己了，游戏结束");
            gameOverDeal(GreedySnakeGameOverResult.HIT_SELF);
            return;
        }
        //判断是否吃到食物
        ifEatFood(headX, nextY);
        //更新蛇的身体
        updateSnakeBodyList(headX, nextY);
        //重绘蛇
        paintSnake();
    }

    private void snakeMoveLeft() {
        //获取当前蛇头
        GreedySnakeBody snakeHead = snakeBodies.get(0);
        //获取蛇头的坐标
        int headX = snakeHead.getBodyX();
        int headY = snakeHead.getBodyY();
        //移动蛇头
        int nextX = headX - 1;
        //判断是否撞墙
        if (nextX < 0) {
            //撞墙了，游戏结束
            System.out.println("撞墙了，游戏结束");
            gameOverDeal(GreedySnakeGameOverResult.HIT_WALL);
            return;
        }
        //判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
        if (backPanels[nextX][headY].getComponentCount() > 0 &&
                backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody) {
            //撞到自己了，游戏结束
            System.out.println("撞到自己了，游戏结束");
            gameOverDeal(GreedySnakeGameOverResult.HIT_SELF);
            return;
        }
        //判断是否吃到食物
        ifEatFood(nextX, headY);
        //更新蛇的身体
        updateSnakeBodyList(nextX, headY);
        //重绘蛇
        paintSnake();
    }

    private void snakeMoveRight() {
        //获取当前蛇头
        GreedySnakeBody snakeHead = snakeBodies.get(0);
        //获取蛇头的坐标
        int headX = snakeHead.getBodyX();
        int headY = snakeHead.getBodyY();
        //移动蛇头
        int nextX = headX + 1;
        //判断是否撞墙
        if (nextX >= COLS) {
            //撞墙了，游戏结束
            System.out.println("撞墙了，游戏结束");
            gameOverDeal(GreedySnakeGameOverResult.HIT_WALL);
            return;
        }
        //判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
        if (backPanels[nextX][headY].getComponentCount() > 0 &&
                backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody) {
            //撞到自己了，游戏结束
            System.out.println("撞到自己了，游戏结束");
            gameOverDeal(GreedySnakeGameOverResult.HIT_SELF);
            return;
        }
        //判断是否吃到食物
        ifEatFood(nextX, headY);
        //更新蛇的身体
        updateSnakeBodyList(nextX, headY);
        //重绘蛇
        paintSnake();
    }

    /**
     * 根据蛇头坐标更新蛇的身体
     *
     * @param x 蛇头的x坐标
     * @param y 蛇头的y坐标
     */
    private void updateSnakeBodyList(int x, int y) {
        //移除原来面板中的蛇身体
        for (GreedySnakeBody snakeBody : snakeBodies) {
            //获取蛇的坐标
            int bodyX = snakeBody.getBodyX();
            int bodyY = snakeBody.getBodyY();
            //获取蛇应该在的面板位置
            JPanel panel = backPanels[bodyX][bodyY];
            //移除面板中的蛇身体
            panel.remove(snakeBody);
            //刷新面板
            panel.revalidate();
            panel.repaint();
        }

        //更新蛇的坐标
        //此处保留了蛇头的位置，即第一个元素0不修改。蛇头的坐标单独修改
        for (int i = snakeBodies.size() - 1; i > 0; i--) {
            //蛇身体的坐标等于上一个蛇身体的坐标
            GreedySnakeBody snakeBody = snakeBodies.get(i);
            GreedySnakeBody snakeBody1 = snakeBodies.get(i - 1);
            snakeBody.setBodyX(snakeBody1.getBodyX());
            snakeBody.setBodyY(snakeBody1.getBodyY());
        }

        //更新蛇头
        GreedySnakeBody greedySnakeBody = new GreedySnakeBody(x, y,true);
        snakeBodies.set(0, greedySnakeBody);
    }

    private void randomProduceFood(){
        int foodX = (int)(Math.random()*ROWS);
        int foodY = (int)(Math.random()*COLS);

        //判断生成的坐标是否与蛇重合
        for (GreedySnakeBody snakeBody : snakeBodies) {
            //获取蛇的坐标
            int bodyX = snakeBody.getBodyX();
            int bodyY = snakeBody.getBodyY();
            if (foodX == bodyX && foodY == bodyY) {
                //重合了，重新生成
                randomProduceFood();
                return;
            }
        }

        //生成食物
        food = food.loadFood(foodX, foodY);
        //将食物添加到面板中
        JPanel jPanel = backPanels[foodX][foodY];
        //使食物填满面板
        jPanel.add(food,BorderLayout.CENTER);
        //刷新面板
        jPanel.revalidate();
        jPanel.repaint();
    }

    /**
     * 当蛇移动后将蛇头的坐标与食物的坐标进行比较，如果相同则吃掉食物
     * @param x 蛇头的x坐标
     * @param y 蛇头的y坐标
     */
    private void ifEatFood(int x,int y){
        if(backPanels[x][y].getComponentCount() > 0 &&
           backPanels[x][y].getComponent(0) instanceof Food
        ){
            //吃到食物了，增加蛇身
            addSnakeBody();
            //分数增加
            this.scoreNum += 10;
            this.score.setText("分数：" + this.scoreNum);
            //当前食物已经被吃掉，重新生成食物
            randomProduceFood();
        }
    }

    private void addSnakeBody(){
         //获取蛇尾
        GreedySnakeBody snakeTail = snakeBodies.get(snakeBodies.size() - 1);
        //获取蛇尾的上一个身体，用于判断当前蛇的位置方向
        GreedySnakeBody snakeSecond = snakeBodies.get(snakeBodies.size() - 2);

        int snakeTailX = snakeTail.getBodyX();
        int snakeTailY = snakeTail.getBodyY();

        int snakeSecondX = snakeSecond.getBodyX();
        int snakeSecondY = snakeSecond.getBodyY();

        //蛇在同一列
        if(snakeTailX == snakeSecondX) {
            if (snakeTailY > snakeSecondY) {
                //蛇正向上移动，添加身体
                snakeBodies.add(new GreedySnakeBody(snakeTailX, snakeTailY - 1));
            } else {
                //蛇正向下移动，添加身体
                snakeBodies.add(new GreedySnakeBody(snakeTailX, snakeTailY + 1));
            }
        }

        //蛇在同一行
        if(snakeTailY == snakeSecondY) {
            if (snakeTailX > snakeSecondX) {
                //蛇正向左移动，添加身体
                snakeBodies.add(new GreedySnakeBody(snakeTailX - 1, snakeTailY));
            } else {
                //蛇正向右移动，添加身体
                snakeBodies.add(new GreedySnakeBody(snakeTailX + 1, snakeTailY));
            }
        }
    }

    private void gameOverDeal(GreedySnakeGameOverResult result){
        //停止计时器
        timer.stop();
        //弹出对话框
        JOptionPane.showMessageDialog(this,
                "游戏结束\r\n分数：" + this.scoreNum+"\r\n原因："+result.getInfo());
        //退出游戏
        System.exit(0);
    }
}
